﻿using System;
using System.Threading;
using System.Collections;

using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

using GLSL;

using MosCraft.Interfaces;
using MosCraft.Utilities;
using MosCraft.Terrain;
using MosCraft.Graphics;


namespace MosCraft
{
    public class World
    {
        public const int MIN_X = -32000000;
        public const int MAX_X = 32000000;
        public const int MIN_Y = 0;
        public const int MAX_Y = 128;
        public const int MIN_Z = -32000000;
        public const int MAX_Z = 32000000;
      
        private MosCraft moscraft;
        public FastRandom rand;
        public volatile ChunkManager Chunks;   
        public WorldProvider worldProvider;
        public WorldRender worldRender;
        
        private string Name;
        private long Seed;
        private long Time;
        private int skylightSubtracted;

        public World(String name, MosCraft moscraft, long seed) 
        {
            this.Name = name;
            this.Seed = seed;
            this.moscraft = moscraft;
            this.rand = new FastRandom();
            this.worldProvider = new WorldProvider(this);
            this.worldRender = new WorldRender(this);
            this.Chunks = new ChunkManager(this);
        }

        /// <summary>
        /// Used to update all world logic
        /// </summary>
        public void Update()
        {   
            long CurrentTime = getWorldTime();
            int skylight = calculateSkylightSubtracted(1.0F);
            if (skylight != skylightSubtracted)
            {
                this.skylightSubtracted = skylight;
            }

            worldRender.UpdateChunks(moscraft.Player);

            CurrentTime += 1;
            setWorldTime(CurrentTime);
        }

        /// <summary>
        /// Called to render chunks
        /// </summary>
        public void Render()
        {
            //render blocks
            worldRender.RenderChunks();
        }

        #region Chunk Management
        public bool doChunksNearChunkExist(int i, int j, int k, int l)
        {
            return checkChunksExist(i - l, j - l, k - l, i + l, j + l, k + l);
        }

        public bool checkChunksExist(int chunkMinX, int chunkMinY, int chunkMinZ, int chunkMaxX, int chunkMaxY, int chunkMaxZ)
        {
            if (chunkMaxY < 0 || chunkMinY >= 128)
            {
                return false;
            }
            chunkMinX >>= 4;
            chunkMinY >>= 4;
            chunkMinZ >>= 4;
            chunkMaxX >>= 4;
            chunkMaxY >>= 4;
            chunkMaxZ >>= 4;
            for (int x = chunkMinX; x <= chunkMaxX; x++)
            {
                for (int z = chunkMinZ; z <= chunkMaxZ; z++)
                {
                    if (!chunkExists(x, z))
                    {
                        return false;
                    }
                }

            }

            return true;
        }

        public bool chunkExists(int x, int z)
        {
            return Chunks.chunkExists(x, z);
        }

        public Chunk getChunkFromBlockCoords(int x, int z)
        {
            return getChunkFromChunkCoords(x >> 4, z >> 4);
        }

        public Chunk getChunkFromChunkCoords(int x, int z)
        {
            return Chunks[x, z];
        }
        #endregion

        #region Block Management
        /// <summary>
        /// Check if a block exist, by looking if if the chunk exist
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>Bool if block exist false if it does not</returns>
        public bool blockExists(int x, int y, int z)
        {
            if (y < 0 || y >= MAX_Y)
            {
                return false;
            }
            else
            {
                return chunkExists(x >> 4, z >> 4);
            }
        }

        /// <summary>
        /// Chesk if the a block is Air
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>If the block at the location is air</returns>
        public bool isAirBlock(int x, int y, int z)
        {
            return getBlockID(x, y, z) == 0;
        }

        /// <summary>
        /// Get the Matrial of the Block
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>Material Enum of the block</returns>
        public Material getBlockMaterial(int x, int y, int z)
        {
            Block block = getBlock(x, y, z);
            if (block == null)
            {
                return Material.Air;
            }
            else
            {
                return block.blockMaterial;
            }
        }

        /// <summary>
        /// Gets te first Id of the block facing the sky
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>The block ID</returns>
        public int getFirstUncoveredBlock(int x, int z)
        {
            int i;

            for (i = 63; !isAirBlock(x, i + 1, z); i++)
            {
            }

            return getBlockID(x, i, z);
        }

        /// <summary>
        /// Checks if a block can "see" the sky
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord></param>
        /// <returns>True on yes and False on no</returns>
        public bool canBlockSeeTheSky(int x, int y, int z)
        {
            return getChunkFromChunkCoords(x >> 4, z >> 4).canBlockSeeTheSky(x & 0xf, y, z & 0xf);
        }

        /// <summary>
        /// Gets th block Id from the chunk by converting the 
        /// Absolute Coord to a local chunk coord
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord></param>
        /// <returns>(int) of the block ID from the chunk</returns>
        public int getBlockID(int x, int y, int z)
        {
            if (x > MAX_X || z > MAX_Z || x < MIN_X || z < MIN_Z)
                return 0;

            if (y < MIN_Y || y > MAX_Y)
            {
                return 0;
            }
            else
            {
                try
                {
                    return getChunkFromChunkCoords(x >> 4, z >> 4).getBlockID(x & 0xf, y, z & 0xf);
                }
                catch
                {
                    return 1;
                }
            }
        }

        /// <summary>
        /// Gets the Height (Y) value for an absolute XZ Coord
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>The int (Y) from the cordinates</returns>
        public int getHeightValue(int x, int z)
        {
            if (x > MAX_X || z > MAX_Z || x < MIN_X || z < MIN_Z)
            {
                return 0;
            }
            if (!chunkExists(x >> 4, z >> 4))
            {
                return 0;
            }
            else
            {
                Chunk chunk = getChunkFromChunkCoords(x >> 4, z >> 4);
                return chunk.getHeightValue(x & 0xf, z & 0xf);
            }
        }

        /// <summary>
        /// Returns the Block Class from the absolute XYZ
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>Block class</returns>
        public Block getBlock(int x, int y, int z)
        {
            int id = this.getBlockID(x, y, z);
            Block block = Block.blocksList[id];
            if(block == null)
            {
                return null;
            }else{
                return block;
            }
        }

        /// <summary>
        /// Check if the block at the location is a normal block
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <returns>Bool on True, Flase on faliure or if the block does not exist</returns>
        public bool isBlockNormalCube(int x, int y, int z)
        {
            Block block = getBlock(x, y, z);
            if (block == null)
            {
                return false;
            }
            else
            {
                return block.isOpaqueCube() && block.renderAsNormalBlock();
            }
        }

        /// <summary>
        /// Sets a block from the absolute to the chunk location
        /// </summary>
        /// <param name="x">Absolute X Coord</param>
        /// <param name="y">Absolute Y Coord</param>
        /// <param name="z">Absolute Z Coord</param>
        /// <param name="blockID">The block ID</param>
        /// <returns>false on Fail, True on success</returns>
        public bool setBlockID(int x, int y, int z, int blockID)
        {
            if (x > MAX_X || z > MAX_Z || x < MIN_X || z < MIN_Z)
                return false;

            if (y < MIN_Y || y > MAX_Y)
            {
                return false;
            }
            {
                getChunkFromChunkCoords(x >> 4, z >> 4).setBlockID(x & 0xf, y, z & 0xf, blockID);
                return true;
            }

        }
        #endregion

        #region World Management
        public int getMaxHeight()
        {
            return 128;
        }

        public int getSeaLevel()
        {
            return this.getMaxHeight() / 2;
        }

        public long getSeed()
        {
            return this.Seed;
        }

        public long getWorldTime()
        {
            return this.Time;
        }

        public void setWorldTime(long time)
        {
            this.Time = time;
        }
        #endregion

        #region collisions
        public ArrayList getCollidingBoundingBoxes(Entity entity, AxisAlignedBB axisalignedbb)
        {
            ArrayList collidingBoundingBoxes = new ArrayList();

            int minX = (int)Math.Floor(axisalignedbb.min.X);
            int maxX = (int)Math.Floor(axisalignedbb.max.X + 1.0D);
            int minY = (int)Math.Floor(axisalignedbb.min.Y);

            int maxY = (int)Math.Floor(axisalignedbb.max.Y + 1.0D);
            int minZ = (int)Math.Floor(axisalignedbb.min.Z);
            int maxZ = (int)Math.Floor(axisalignedbb.max.Z + 1.0D);

            for (int x = minX; x < maxX; x++)
            {
                for (int z = minZ; z < maxZ; z++)
                {
                    if (!blockExists(x, (1 << 7) / 2, z))
                    {
                        continue;
                    }

                    for (int y = minY - 1; y < maxY; y++)
                    {
                        Block block = Block.blocksList[getBlockID(x, y, z)];
                        
                        if (block != null)
                        {
                           block.getCollidingBoundingBoxes(this, x, y, z, axisalignedbb, ref collidingBoundingBoxes);
                        }
                    }

                }

            }

            return collidingBoundingBoxes;
        }
        #endregion

        #region SkyBox
        public int calculateSkylightSubtracted(float par1)
        {
            float f = getCelestialAngle(par1);

            float f1 = 1.0F - (float)(Math.Cos(f * MathHelper.Pi * 2.0F) * 2.0F + 0.5F);

            if (f1 < 0.0F)
                f1 = 0.0F;

            if (f1 > 1.0F)
                f1 = 1.0F;

            f1 = 1.0F - f1;
            return (int)(f1 * 11F);
        }

        public float getCelestialAngle(float par1)
        {
            return worldProvider.calculateCelestialAngle(this.Time, par1);
        }

        public Vector3d drawClouds(float f)
        {
            long cloudColour = 0xffffffL;
            float CelestialAngle = getCelestialAngle(f);
            float angle = QuickMath.Cos(CelestialAngle * MathHelper.Pi * 2.0F) * 2.0F + 0.5F;

            if (angle < 0.0F)
            {
                angle = 0.0F;
            }
            if (angle > 1.0F)
            {
                angle = 1.0F;
            }

            float x = (float)(cloudColour >> 16 & 255L) / 255F;
            float y = (float)(cloudColour >> 8 & 255L) / 255F;
            float z = (float)(cloudColour & 255L) / 255F;

            x *= angle * 0.9F + 0.1F;
            y *= angle * 0.9F + 0.1F;
            z *= angle * 0.85F + 0.15F;

            return new Vector3d(x, y, z);
        }

        public Color4 getFogColor(float par1)
        {
            float angle = getCelestialAngle(par1);
            return worldProvider.getFogColor(angle, par1);
        }

        public float getStarBrightness(float par1)
        {
            float angle = getCelestialAngle(par1);
            float brightness = 1.0F - (QuickMath.Cos(angle * (float)Math.PI * 2.0F) * 2.0F + 0.75F);

            if (brightness < 0.0F)
            {
                brightness = 0.0F;
            }

            if (brightness > 1.0F)
            {
                brightness = 1.0F;
            }

            return brightness * brightness * 0.6F;
        }
        #endregion

        /// <summary>
        /// Creates an Intersecting line that then traces through
        /// 3D SPPPAAACCEEEEE, used for the block selection tool
        /// </summary>
        /// <param name="Direction">The Camera Direction</param>
        /// <param name="Look">The Camera Look</param>
        /// <param name="rayDistance">The distance to trace to</param>
        /// <returns>RayTraceObject Contains the block ID, its postion and space</returns>
        public RayTraceObject rayTraceBlocks(Vector3d Direction, Vector3d Look, double rayDistance)
        {
            if (Double.IsNaN(Direction.X) || Double.IsNaN(Direction.Y) || Double.IsNaN(Direction.Z))
            {
                return null;
            }

            if (Double.IsNaN(Look.X) || Double.IsNaN(Look.Y) || Double.IsNaN(Look.Z))
            {
                return null;
            }

            foreach (RayTracer.Hit hit in RayTracer.trace(Direction, Look, rayDistance))
            {
                //First check that we don't select air
                if (!isAirBlock(hit.x, hit.y, hit.z))
                {
                    Block focusedBlock = getBlock(hit.x, hit.y, hit.z);

                    //Check that where looking at a normal block
                    if (focusedBlock.getSelectedBoundingBoxFromPool(this, hit.x, hit.y, hit.z) != null)
                    {
                        return new RayTraceObject(hit.x, hit.y, hit.z, hit.face, focusedBlock.blockID);
                    }
                }
            }

            return null;
        }
    }
}